using System;
using System.Collections.Generic;

namespace ZFramework
{
	public struct AABB
	{
		public Vec3 position;
		private Vec3 size;

		public Vec3 Min
		{
			readonly get => position;
			set
			{
				size -= value - position;
				position = value;
				Validate();
			}
		}

		public Vec3 Max
		{
			readonly get => position + size;
			set
			{
				size = value - position;
				Validate();
			}
		}

		public Vec3 Size
		{
			readonly get => size;
			set
			{
				size = value;
				Validate();
			}
		}

		public AABB(Vec3 p1, Vec3 p2)
		{
			var xMin = MathF.Min(p1.x, p2.x);
			var xMax = MathF.Max(p1.x, p2.x);
			var yMin = MathF.Min(p1.y, p2.y);
			var yMax = MathF.Max(p1.y, p2.y);
			var zMin = MathF.Min(p1.z, p2.z);
			var zMax = MathF.Max(p1.z, p2.z);

			position = new(xMin, yMin, zMin);
			size = new Vec3(xMax, yMax, zMax) - position;
		}

		private void Validate()
		{
			for (var i = 0; i < 3; i++)
			{
				if (size[i] < 0.0f)
				{
					position[i] += size[i];
					size[i] = -size[i];
				}
			}
		}

		public readonly IEnumerable<Vec3> GetVertices()
		{
			Vec3[] mm = [Min, Max];
			for (byte c = 0; c < 8; c++)
			{
				yield return new(mm[c & 1].x, mm[(c >> 1) & 1].y, mm[(c >> 2) & 1].z);
			}
		}

		public HitResult? Intersects(Ray ray, float maxDistance)
		{
			static bool Less((float v, Facing) a, (float v, Facing) b) => a.v < b.v;

			var (txMin, txMax) = MathUtil.MinMax(
				((Min.x - ray.Origin.x) / ray.Direction.x, Facing.West),
				((Max.x - ray.Origin.x) / ray.Direction.x, Facing.East),
				Less
			);

			var (tyMin, tyMax) = MathUtil.MinMax(
				((Min.y - ray.Origin.y) / ray.Direction.y, Facing.Down),
				((Max.y - ray.Origin.y) / ray.Direction.y, Facing.Up),
				Less
			);

			var ixyMin = MathUtil.Max(txMin, tyMin, Less);
			var ixyMax = MathUtil.Min(txMax, tyMax, Less);

			if (Less(ixyMax, ixyMin)) return null;

			var (tzMin, tzMax) = MathUtil.MinMax(
				((Min.z - ray.Origin.z) / ray.Direction.z, Facing.North),
				((Max.z - ray.Origin.z) / ray.Direction.z, Facing.South),
				Less
			);

			var ixyzMin = MathUtil.Max(ixyMin, tzMin, Less);
			var ixyzMax = MathUtil.Min(ixyMax, tzMax, Less);

			if (ixyzMin.Item1 > maxDistance || Less(ixyzMax, ixyzMin)) return null;

			return new HitResult
			{
				position = ray.Origin + ixyzMin.Item1 * ray.Direction,
				normal = ixyzMin.Item2 switch
				{
					Facing.East => Vec3.Right,
					Facing.Up => Vec3.Up,
					Facing.North => Vec3.Forward,
					Facing.West => Vec3.Left,
					Facing.Down => Vec3.Down,
					Facing.South => Vec3.Back,
					_ => Vec3.One,
				}
			};
		}
	}
}
